--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit a067afaa1f5ef7dfe5d6fbaba0c6986f2c7b0998
Parents : 5e5ce25
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-02T04:38:28-05:00
feat(tests): add/update tests
Changes
8 files changed, 254 insertions(+), 9 deletions(-)
Diff
diff --git a/tests/backend/test_announce_dao_extended.py b/tests/backend/test_announce_dao_extended.py
index a1fc92a5..ec069889 100644
--- a/tests/backend/test_announce_dao_extended.py
+++ b/tests/backend/test_announce_dao_extended.py
@@ -141,3 +141,16 @@ def test_get_announce_count_by_aspect(announce_dao):
assert announce_dao.get_announce_count_by_aspect("lxmf.delivery") == 2
assert announce_dao.get_announce_count_by_aspect("nomadnetwork.node") == 1
assert announce_dao.get_announce_count_by_aspect("lxmf.propagation") == 0
+
+
+def test_get_favourite_by_destination_hash(announce_dao):
+ assert announce_dao.get_favourite_by_destination_hash("missing") is None
+ announce_dao.upsert_favourite("dh1", "Original", "nomadnetwork.node")
+ row = announce_dao.get_favourite_by_destination_hash("dh1")
+ assert row["destination_hash"] == "dh1"
+ assert row["display_name"] == "Original"
+ assert row["aspect"] == "nomadnetwork.node"
+ announce_dao.upsert_custom_display_name("dh1", "Renamed")
+ announce_dao.upsert_favourite("dh1", "Renamed", row["aspect"])
+ row2 = announce_dao.get_favourite_by_destination_hash("dh1")
+ assert row2["display_name"] == "Renamed"
diff --git a/tests/backend/test_http_url_guard.py b/tests/backend/test_http_url_guard.py
index 780eac12..14630c78 100644
--- a/tests/backend/test_http_url_guard.py
+++ b/tests/backend/test_http_url_guard.py
@@ -5,6 +5,7 @@ import pytest
from meshchatx.src.backend.http_url_guard import (
UnsafeOutboundUrlError,
normalize_loopback_http_service_base,
+ normalize_libretranslate_http_service_base,
)
@@ -72,3 +73,66 @@ def test_normalize_accepts_loopback_variants(edge):
def test_normalize_rejects_scheme_or_crlf_injection(bad):
with pytest.raises(UnsafeOutboundUrlError):
normalize_loopback_http_service_base(bad)
+
+
+def test_normalize_libretranslate_public_https():
+ assert normalize_libretranslate_http_service_base(
+ "https://libretranslate.com/path"
+ ) == ("https://libretranslate.com")
+
+
+def test_normalize_libretranslate_remote_host_port_strip_path():
+ assert normalize_libretranslate_http_service_base(
+ "http://superfishy.example:5002/languages",
+ ) == ("http://superfishy.example:5002")
+
+
+def test_normalize_libretranslate_private_and_loopback_ips():
+ assert normalize_libretranslate_http_service_base("http://10.20.30.40:5123/") == (
+ "http://10.20.30.40:5123"
+ )
+ assert normalize_libretranslate_http_service_base("http://127.0.0.1:5000") == (
+ "http://127.0.0.1:5000"
+ )
+
+
+@pytest.mark.parametrize(
+ "bad",
+ [
+ "http://169.254.169.254/latest",
+ "http://239.255.0.1:5000/",
+ "http://0.0.0.0/",
+ "http://240.0.0.1:1",
+ ],
+)
+def test_normalize_libretranslate_rejects_ssrf_lit_ips(bad):
+ with pytest.raises(UnsafeOutboundUrlError):
+ normalize_libretranslate_http_service_base(bad)
+
+
+@pytest.mark.parametrize(
+ "bad",
+ [
+ "ftp://example.com/",
+ "http://user:pass@192.168.1.1:5000",
+ ],
+)
+def test_normalize_libretranslate_rejects_scheme_or_creds(bad):
+ with pytest.raises(UnsafeOutboundUrlError):
+ normalize_libretranslate_http_service_base(bad)
+
+
+@pytest.mark.parametrize(
+ "bad",
+ ["", " ", "http+unix://%2Ftmp%2Fs.sock"],
+)
+def test_normalize_libretranslate_rejects_scheme_or_empty(bad):
+ with pytest.raises(UnsafeOutboundUrlError):
+ normalize_libretranslate_http_service_base(bad)
+
+
+def test_normalize_libretranslate_rejects_encoded_crlf_in_host():
+ with pytest.raises(UnsafeOutboundUrlError):
+ normalize_libretranslate_http_service_base(
+ "http://127.0.0.1%0d%0a.evil.com:80/"
+ )
diff --git a/tests/backend/test_meshchat_coverage.py b/tests/backend/test_meshchat_coverage.py
index 5ca2ebd7..78151b93 100644
--- a/tests/backend/test_meshchat_coverage.py
+++ b/tests/backend/test_meshchat_coverage.py
@@ -73,6 +73,38 @@ async def test_update_config_theme(mock_app):
mock_app.config.theme.set.assert_called_with("dark")
+@pytest.mark.asyncio
+async def test_update_config_libretranslate_api_key(mock_app):
+ mock_app.send_config_to_websocket_clients = MagicMock(return_value=asyncio.Future())
+ mock_app.send_config_to_websocket_clients.return_value.set_result(None)
+ mock_app.config.libretranslate_api_key = MagicMock()
+ mock_app.translator_handler = MagicMock()
+ await mock_app.update_config({"libretranslate_api_key": " sek "})
+ mock_app.config.libretranslate_api_key.set.assert_called_once_with("sek")
+ assert mock_app.translator_handler.libretranslate_api_key == "sek"
+
+
+@pytest.mark.asyncio
+async def test_update_config_libretranslate_api_key_empty_clears(mock_app):
+ mock_app.send_config_to_websocket_clients = MagicMock(return_value=asyncio.Future())
+ mock_app.send_config_to_websocket_clients.return_value.set_result(None)
+ mock_app.config.libretranslate_api_key = MagicMock()
+ mock_app.translator_handler = MagicMock()
+ await mock_app.update_config({"libretranslate_api_key": ""})
+ mock_app.config.libretranslate_api_key.set.assert_called_once_with(None)
+ assert mock_app.translator_handler.libretranslate_api_key is None
+
+
+@pytest.mark.asyncio
+async def test_update_config_libretranslate_api_key_length_limit(mock_app):
+ mock_app.send_config_to_websocket_clients = MagicMock(return_value=asyncio.Future())
+ mock_app.send_config_to_websocket_clients.return_value.set_result(None)
+ mock_app.config.libretranslate_api_key = MagicMock()
+ mock_app.translator_handler = MagicMock()
+ with pytest.raises(ValueError):
+ await mock_app.update_config({"libretranslate_api_key": "z" * 513})
+
+
def test_get_config_dict_no_context(mock_app):
mock_app.current_context = None
assert mock_app.get_config_dict() == {}
@@ -149,6 +181,7 @@ def test_get_config_dict_basic(mock_app):
"translator_argos_enabled",
"translator_libretranslate_enabled",
"libretranslate_url",
+ "libretranslate_api_key",
"desktop_open_calls_in_separate_window",
"desktop_hardware_acceleration_enabled",
"blackhole_integration_enabled",
diff --git a/tests/backend/test_translator_handler.py b/tests/backend/test_translator_handler.py
index 2f360579..51e47736 100644
--- a/tests/backend/test_translator_handler.py
+++ b/tests/backend/test_translator_handler.py
@@ -68,6 +68,8 @@ class TestTranslatorHandler(unittest.TestCase):
result = self.handler.translate_text("Hello", "en", "de", use_argos=False)
self.assertEqual(result["translated_text"], "Hallo")
self.assertEqual(result["source"], "libretranslate")
+ body = mock_session.post.call_args.kwargs.get("json")
+ self.assertNotIn("api_key", body)
if __name__ == "__main__":
diff --git a/tests/backend/test_translator_handler_extended.py b/tests/backend/test_translator_handler_extended.py
index 82629a96..1e1d15ac 100644
--- a/tests/backend/test_translator_handler_extended.py
+++ b/tests/backend/test_translator_handler_extended.py
@@ -4,7 +4,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
-from meshchatx.src.backend.translator_handler import TranslatorHandler
+from meshchatx.src.backend.translator_handler import (
+ TranslatorHandler,
+ _normalize_optional_libretranslate_api_key,
+)
def test_translator_handler_init():
@@ -17,6 +20,11 @@ def test_translator_handler_init():
assert handler.translator_argos_enabled is True
+def test_translator_handler_init_optional_api_key_stripped():
+ handler = TranslatorHandler(libretranslate_api_key=" trim ")
+ assert handler.libretranslate_api_key == "trim"
+
+
def test_get_supported_languages_no_backends():
handler = TranslatorHandler()
handler.has_requests = False
@@ -202,7 +210,7 @@ def test_language_code_to_name():
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
-def test_get_supported_languages_skips_non_loopback_stored_url(mock_session_cls):
+def test_get_supported_languages_skips_link_local_lit_stored_url(mock_session_cls):
handler = TranslatorHandler(
libretranslate_url="http://169.254.169.254/",
translator_libretranslate_enabled=True,
@@ -215,15 +223,30 @@ def test_get_supported_languages_skips_non_loopback_stored_url(mock_session_cls)
mock_session_cls.assert_not_called()
-def test_translate_text_rejects_non_loopback_libre_url():
+@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
+def test_translate_text_accepts_remote_libre_url(mock_session_cls):
+ mock_response = MagicMock()
+ mock_response.status = 200
+ mock_response.json = AsyncMock(return_value={"translatedText": "Hallo"})
+ mock_post = MagicMock()
+ mock_post.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_post.__aexit__ = AsyncMock(return_value=None)
+ mock_session = MagicMock()
+ mock_session.post = MagicMock(return_value=mock_post)
+ mock_session.__aenter__ = AsyncMock(return_value=mock_session)
+ mock_session.__aexit__ = AsyncMock(return_value=None)
+ mock_session_cls.return_value = mock_session
+
handler = TranslatorHandler(
libretranslate_url="http://example.com:5000/",
translator_libretranslate_enabled=True,
translator_argos_enabled=False,
)
handler.has_requests = True
- with pytest.raises(ValueError, match="URL host must be"):
- handler.translate_text("Hello", "en", "de", use_argos=False)
+ result = handler.translate_text("Hello", "en", "de", use_argos=False)
+ assert result["translated_text"] == "Hallo"
+ called_url = mock_session.post.call_args.args[0]
+ assert called_url.startswith("http://example.com:5000")
def test_get_translator_languages_response_explicit_bad_override_raises():
@@ -232,7 +255,105 @@ def test_get_translator_languages_response_explicit_bad_override_raises():
translator_libretranslate_enabled=True,
)
handler.has_requests = True
- with pytest.raises(ValueError, match="URL host must be"):
+ with pytest.raises(ValueError, match="IPv4 link-local"):
handler.get_translator_languages_response(
- libretranslate_url="http://metadata.example/latest",
+ libretranslate_url="http://169.254.169.254:5000",
)
+
+
+def test_normalize_optional_libretranslate_api_key():
+ assert _normalize_optional_libretranslate_api_key(None) is None
+ assert _normalize_optional_libretranslate_api_key(" ") is None
+ assert _normalize_optional_libretranslate_api_key(" xyz ") == "xyz"
+ with pytest.raises(ValueError):
+ _normalize_optional_libretranslate_api_key("k" * 513)
+
+
+@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
+def test_get_supported_languages_sends_api_key_for_languages_when_set(mock_session_cls):
+ mock_response = MagicMock()
+ mock_response.status = 200
+ mock_response.json = AsyncMock(return_value=[{"code": "en", "name": "English"}])
+ mock_get = MagicMock()
+ mock_get.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_get.__aexit__ = AsyncMock(return_value=None)
+ mock_session = MagicMock()
+ mock_session.get = MagicMock(return_value=mock_get)
+ mock_session.__aenter__ = AsyncMock(return_value=mock_session)
+ mock_session.__aexit__ = AsyncMock(return_value=None)
+ mock_session_cls.return_value = mock_session
+
+ handler = TranslatorHandler(
+ translator_libretranslate_enabled=True,
+ libretranslate_api_key="srv-key",
+ )
+ handler.has_requests = True
+ handler.has_argos = False
+ handler.has_argos_lib = False
+ handler.has_argos_cli = False
+ handler.get_supported_languages()
+
+ kw = mock_session.get.call_args.kwargs
+ assert kw.get("params") == {"api_key": "srv-key"}
+
+
+@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
+def test_translate_sends_optional_api_key_in_json_body(mock_session_cls):
+ mock_response = MagicMock()
+ mock_response.status = 200
+ mock_response.json = AsyncMock(
+ return_value={
+ "translatedText": "Bonjour",
+ "detectedLanguage": {"language": "en"},
+ },
+ )
+ mock_post_ctx = MagicMock()
+ mock_post_ctx.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_post_ctx.__aexit__ = AsyncMock(return_value=None)
+ mock_session = MagicMock()
+ mock_session.post = MagicMock(return_value=mock_post_ctx)
+ mock_session.__aenter__ = AsyncMock(return_value=mock_session)
+ mock_session.__aexit__ = AsyncMock(return_value=None)
+ mock_session_cls.return_value = mock_session
+
+ handler = TranslatorHandler(
+ translator_libretranslate_enabled=True,
+ libretranslate_api_key="abc123",
+ translator_argos_enabled=False,
+ )
+ handler.has_requests = True
+ handler.translate_text("Hello", "en", "fr", use_argos=False)
+
+ body = mock_session.post.call_args.kwargs.get("json")
+ assert body.get("api_key") == "abc123"
+
+
+@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")
+def test_translate_explicit_api_key_overrides_handler_default(mock_session_cls):
+ mock_response = MagicMock()
+ mock_response.status = 200
+ mock_response.json = AsyncMock(return_value={"translatedText": "Hi"})
+ mock_post_ctx = MagicMock()
+ mock_post_ctx.__aenter__ = AsyncMock(return_value=mock_response)
+ mock_post_ctx.__aexit__ = AsyncMock(return_value=None)
+ mock_session = MagicMock()
+ mock_session.post = MagicMock(return_value=mock_post_ctx)
+ mock_session.__aenter__ = AsyncMock(return_value=mock_session)
+ mock_session.__aexit__ = AsyncMock(return_value=None)
+ mock_session_cls.return_value = mock_session
+
+ handler = TranslatorHandler(
+ translator_libretranslate_enabled=True,
+ libretranslate_api_key="stored",
+ translator_argos_enabled=False,
+ )
+ handler.has_requests = True
+ handler.translate_text(
+ "Hello",
+ "en",
+ "de",
+ use_argos=False,
+ libretranslate_api_key=" one-off ",
+ )
+ body = mock_session.post.call_args.kwargs.get("json")
+ assert body.get("api_key") == "one-off"
diff --git a/tests/frontend/ConversationViewerButtons.test.js b/tests/frontend/ConversationViewerButtons.test.js
index fa16c808..f88e82a7 100644
--- a/tests/frontend/ConversationViewerButtons.test.js
+++ b/tests/frontend/ConversationViewerButtons.test.js
@@ -365,7 +365,7 @@ describe("ConversationViewer.vue button interactions", () => {
"/api/v1/translator/translate",
expect.objectContaining({
text: "hello",
- source_lang: "en",
+ source_lang: "auto",
target_lang: "de",
use_argos: true,
})
diff --git a/tests/frontend/TranslatorPage.test.js b/tests/frontend/TranslatorPage.test.js
index 1195e844..787852b6 100644
--- a/tests/frontend/TranslatorPage.test.js
+++ b/tests/frontend/TranslatorPage.test.js
@@ -20,6 +20,7 @@ describe("TranslatorPage.vue", () => {
translator_argos_enabled: true,
translator_libretranslate_enabled: true,
libretranslate_url: "http://localhost:5000",
+ libretranslate_api_key: null,
},
},
});
@@ -49,10 +50,19 @@ describe("TranslatorPage.vue", () => {
});
const mountTranslatorPage = () => {
+ const tMap = {
+ "translator.api_server": "LibreTranslate API Server",
+ "translator.api_server_description":
+ "Enter the base URL of your LibreTranslate server (e.g., http://localhost:5000)",
+ "translator.api_key_optional": "LibreTranslate API key (optional)",
+ "translator.api_key_placeholder": "Leave empty unless your provider requires one",
+ "translator.api_key_description": "If required, LibreTranslate expects api_key in the JSON translate body.",
+ "translator.failed_load_languages": "Failed",
+ };
return mount(TranslatorPage, {
global: {
mocks: {
- $t: (key) => key,
+ $t: (key) => (key in tMap ? tMap[key] : key),
},
stubs: {
MaterialDesignIcon: {
@@ -80,6 +90,7 @@ describe("TranslatorPage.vue", () => {
translator_argos_enabled: false,
translator_libretranslate_enabled: false,
libretranslate_url: "http://127.0.0.1:5000",
+ libretranslate_api_key: null,
},
},
});
diff --git a/tests/frontend/fixtures/settingsPageTestApi.js b/tests/frontend/fixtures/settingsPageTestApi.js
index c2bb2217..0e7f66a7 100644
--- a/tests/frontend/fixtures/settingsPageTestApi.js
+++ b/tests/frontend/fixtures/settingsPageTestApi.js
@@ -91,6 +91,7 @@ export function buildFullServerConfig(overrides = {}) {
translator_argos_enabled: false,
translator_libretranslate_enabled: false,
libretranslate_url: "http://localhost:5000",
+ libretranslate_api_key: null,
desktop_open_calls_in_separate_window: false,
desktop_hardware_acceleration_enabled: true,
blackhole_integration_enabled: true,
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────